import logging
from datetime import timedelta
from typing import NoReturn

from sqlalchemy import and_, case, or_, select
from sqlalchemy.exc import DBAPIError, IntegrityError
from sqlalchemy.orm import joinedload

from src.application.common.exceptions import RepoError
from src.application.order.dto.order import OrderDTO
from src.application.order.enums import OrderPeriod, OrderStatus, OrderType
from src.application.order.exceptions import OrderIdAlreadyExist, OrderIdNotExist
from src.application.order.interfaces import GetOrdersFilters, OrderReader, OrderRepo
from src.domain.common.constants import Empty
from src.domain.order.aggregates import OrderAggregate
from src.domain.order.value_objects import OrderId
from src.infrastructure.date_helpers import get_date_time
from src.infrastructure.db.converters.order import (
    convert_db_model_to_order_dto,
    convert_db_model_to_order_entity,
    convert_order_entity_to_db_model,
)
from src.infrastructure.db.exception_mapper import exception_mapper
from src.infrastructure.db.models import Order, User
from src.infrastructure.db.repositories.base import SQLAlchemyRepo

logger = logging.getLogger(__name__)


class OrderReaderImpl(SQLAlchemyRepo, OrderReader):
    @exception_mapper
    async def get_order_by_id(self, order_id: int) -> OrderDTO:
        order: Order | None = await self._session.scalar(
            select(Order).where(
                Order.id == order_id,
            )
        )
        if order is None:
            raise OrderIdNotExist(order_id)

        # TODO Добавить joined
        # if joined:
        #     q = q.options(
        #         joinedload(Order.proxies),
        #         joinedload(Order.category),
        #         joinedload(Order.site),
        #         joinedload(Order.provider),
        #         joinedload(Order.country),
        #         joinedload(Order.user),
        #         joinedload(Order.allowed_ips),
        #         joinedload(Order.vpn_server),
        #     )

        return convert_db_model_to_order_dto(order)

    @exception_mapper
    async def get_orders(self, filters: GetOrdersFilters) -> list[OrderDTO]:
        query = select(Order)

        if filters.tg_user_id != Empty.UNSET:
            query = query.outerjoin(Order.user).filter(User.telegram_id == filters.tg_user_id)

        if filters.order_status != Empty.UNSET:
            query = query.filter(Order.status == filters.order_status)

        if filters.order_type != Empty.UNSET:
            query = query.filter(Order.type == filters.order_type)

        # TODO Заменить joinedload на отдельный запрос
        query = query.order_by(Order.was_created.desc()).options(joinedload(Order.proxies))

        result = await self._session.scalars(query)
        orders: list[Order] = list(result.unique())

        return [convert_db_model_to_order_dto(order) for order in orders]

    @exception_mapper
    async def get_user_subscription_order(self, tg_user_id: int) -> OrderDTO:
        order: Order = await self._session.scalar(
            select(Order)
            .outerjoin(Order.user)
            .filter(
                and_(
                    User.telegram_id == tg_user_id,
                    Order.type == OrderType.subscription,
                    Order.status == OrderStatus.active,
                )
            )
        )
        return convert_db_model_to_order_dto(order)

    @exception_mapper
    async def get_expiring_orders(self, autorenew: bool = False) -> list[OrderDTO]:
        """
        Получаем список заказов с истекающим сроком действия
        Нужно для рассылки уведомлений, а также для автопродления
        :param bool autorenew: Получаем заказы с включенным автообновлением
        """

        cur_date = get_date_time()

        # Тестовые заказы и заказы с сроком аренды в 4 часа не продлеваются и не уведомляются
        query = (
            select(Order)
            .filter(
                and_(
                    Order.type != OrderType.vpn_router,
                    Order.period != OrderPeriod.four_hours,
                    Order.period != OrderPeriod.twenty_minutes,
                    Order.status == OrderStatus.active,
                )
            )
            .options(joinedload(Order.user))
        )
        if autorenew:
            # Получаем заказы для автообновления, до конца которых осталось меньше 6 часов
            query = query.filter(
                and_(
                    Order.auto_renew == True,
                    Order.expiring_on <= cur_date + timedelta(hours=6),
                )
            )
        else:
            # Получаем заказы для рассылки, до конца которых осталось меньше 24 часов
            # За сутки пользователю придет 3 напоминания
            # Заказы с сроком аренды в 1 день уведомляются 1 раз
            query = query.filter(
                and_(
                    case(
                        (
                            Order.period == OrderPeriod.one_day,
                            Order.expiring_on <= cur_date + timedelta(hours=4),
                        ),
                        else_=Order.expiring_on <= cur_date + timedelta(hours=24),
                    ),
                    or_(
                        Order.last_notified <= cur_date - timedelta(hours=8),
                        Order.last_notified == None,
                    ),
                )
            )

        result = await self._session.scalars(query)
        orders: list[Order] = list(result)

        return [convert_db_model_to_order_dto(order) for order in orders]

    @exception_mapper
    async def get_expired_orders(self) -> list[OrderDTO]:
        """Получаем просроченные заказы"""

        cur_date = get_date_time()

        query = select(Order).where(
            and_(
                Order.type != OrderType.vpn_router,
                Order.status == OrderStatus.active,
                Order.expiring_on < cur_date,
            )
        )

        result = await self._session.scalars(query)
        orders: list[Order] = list(result)

        return [convert_db_model_to_order_dto(order) for order in orders]


class OrderRepoImpl(SQLAlchemyRepo, OrderRepo):
    @exception_mapper
    async def acquire_order_by_id(self, order_id: OrderId) -> OrderAggregate:
        order: Order | None = await self._session.scalar(
            select(Order)
            .where(
                Order.id == order_id.value,
            )
            .with_for_update()
        )
        # if user is None:
        #     raise UserIdNotExist(user_tg_id.value)

        return convert_db_model_to_order_entity(order)

    @exception_mapper
    async def create_order(self, order: OrderAggregate) -> None:
        db_order = convert_order_entity_to_db_model(order)
        self._session.add(db_order)
        try:
            await self._session.flush((db_order,))
        except IntegrityError as err:
            self._parse_error(err, db_order)

    @exception_mapper
    async def update_order(self, order: OrderAggregate) -> None:
        db_order = convert_order_entity_to_db_model(order)
        print(db_order)
        try:
            await self._session.merge(db_order)
        except IntegrityError as err:
            self._parse_error(err, db_order)

    def _parse_error(self, err: DBAPIError, order: OrderAggregate) -> NoReturn:
        logger.info(err.__cause__.__cause__.constraint_name)

        match err.__cause__.__cause__.constraint_name:  # type: ignore
            case "pk_order":
                raise OrderIdAlreadyExist(order.order_id.value) from err
            case _:
                raise RepoError from err
